//
// Copyright (C) 2024 EA group inc.
// Author: Jeff.li lijippy@163.com
// All rights reserved.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//

#include <turbo/log/internal/log_format.h>

#include <string.h>

#ifdef _MSC_VER
#include <winsock2.h>  // For timeval
#else
#include <sys/time.h>
#endif

#include <cstddef>
#include <cstdint>
#include <limits>
#include <string>
#include <type_traits>

#include <turbo/base/config.h>
#include <turbo/base/log_severity.h>
#include <turbo/base/optimization.h>
#include <turbo/log/internal/append_truncated.h>
#include <turbo/log/internal/config.h>
#include <turbo/log/internal/globals.h>
#include <turbo/strings/numbers.h>
#include <turbo/strings/str_format.h>
#include <turbo/strings/string_view.h>
#include <turbo/times/civil_time.h>
#include <turbo/times/time.h>
#include <turbo/container/span.h>

namespace turbo {
TURBO_NAMESPACE_BEGIN
namespace log_internal {
namespace {

// This templated function avoids compiler warnings about tautological
// comparisons when log_internal::Tid is unsigned. It can be replaced with a
// constexpr if once the minimum C++ version Turbo supports is C++17.
template <typename T>
inline std::enable_if_t<!std::is_signed<T>::value>
PutLeadingWhitespace(T tid, char*& p) {
  if (tid < 10) *p++ = ' ';
  if (tid < 100) *p++ = ' ';
  if (tid < 1000) *p++ = ' ';
  if (tid < 10000) *p++ = ' ';
  if (tid < 100000) *p++ = ' ';
  if (tid < 1000000) *p++ = ' ';
}

template <typename T>
inline std::enable_if_t<std::is_signed<T>::value>
PutLeadingWhitespace(T tid, char*& p) {
  if (tid >= 0 && tid < 10) *p++ = ' ';
  if (tid > -10 && tid < 100) *p++ = ' ';
  if (tid > -100 && tid < 1000) *p++ = ' ';
  if (tid > -1000 && tid < 10000) *p++ = ' ';
  if (tid > -10000 && tid < 100000) *p++ = ' ';
  if (tid > -100000 && tid < 1000000) *p++ = ' ';
}

// The fields before the filename are all fixed-width except for the thread ID,
// which is of bounded width.
size_t FormatBoundedFields(turbo::LogSeverity severity, turbo::Time timestamp,
                           log_internal::Tid tid, turbo::span<char>& buf) {
  constexpr size_t kBoundedFieldsMaxLen =
      sizeof("SMMDD HH:MM:SS.NNNNNN  ") +
      (1 + std::numeric_limits<log_internal::Tid>::digits10 + 1) - sizeof("");
  if (TURBO_UNLIKELY(buf.size() < kBoundedFieldsMaxLen)) {
    // We don't bother trying to truncate these fields if the buffer is too
    // short (or almost too short) because it would require doing a lot more
    // length checking (slow) and it should never happen.  A 15kB buffer should
    // be enough for anyone.  Instead we mark `buf` full without writing
    // anything.
    buf.remove_suffix(buf.size());
    return 0;
  }

  // We can't call turbo::LocalTime(), localtime_r(), or anything else here that
  // isn't async-signal-safe. We can only use the time zone if it has already
  // been loaded.
  const turbo::TimeZone* tz = turbo::log_internal::TimeZone();
  if (TURBO_UNLIKELY(tz == nullptr)) {
    // If a time zone hasn't been set yet because we are logging before the
    // logging library has been initialized, we fallback to a simpler, slower
    // method. Just report the raw Unix time in seconds. We cram this into the
    // normal time format for the benefit of parsers.
    auto tv = turbo::Time::to_timeval(timestamp);
    int snprintf_result = turbo::SNPrintF(
        buf.data(), buf.size(), "%c0000 00:00:%02d.%06d %7d ",
        turbo::LogSeverityName(severity)[0], static_cast<int>(tv.tv_sec),
        static_cast<int>(tv.tv_usec), static_cast<int>(tid));
    if (snprintf_result >= 0) {
      buf.remove_prefix(static_cast<size_t>(snprintf_result));
      return static_cast<size_t>(snprintf_result);
    }
    return 0;
  }

  char* p = buf.data();
  *p++ = turbo::LogSeverityName(severity)[0];
  const turbo::TimeZone::CivilInfo ci = tz->at(timestamp);
  turbo::numbers_internal::PutTwoDigits(static_cast<uint32_t>(ci.cs.month()), p);
  p += 2;
  turbo::numbers_internal::PutTwoDigits(static_cast<uint32_t>(ci.cs.day()), p);
  p += 2;
  *p++ = ' ';
  turbo::numbers_internal::PutTwoDigits(static_cast<uint32_t>(ci.cs.hour()), p);
  p += 2;
  *p++ = ':';
  turbo::numbers_internal::PutTwoDigits(static_cast<uint32_t>(ci.cs.minute()),
                                       p);
  p += 2;
  *p++ = ':';
  turbo::numbers_internal::PutTwoDigits(static_cast<uint32_t>(ci.cs.second()),
                                       p);
  p += 2;
  *p++ = '.';
  const int64_t usecs = turbo::Duration::to_microseconds(ci.subsecond);
  turbo::numbers_internal::PutTwoDigits(static_cast<uint32_t>(usecs / 10000), p);
  p += 2;
  turbo::numbers_internal::PutTwoDigits(static_cast<uint32_t>(usecs / 100 % 100),
                                       p);
  p += 2;
  turbo::numbers_internal::PutTwoDigits(static_cast<uint32_t>(usecs % 100), p);
  p += 2;
  *p++ = ' ';
  PutLeadingWhitespace(tid, p);
  p = turbo::numbers_internal::FastIntToBuffer(tid, p);
  *p++ = ' ';
  const size_t bytes_formatted = static_cast<size_t>(p - buf.data());
  buf.remove_prefix(bytes_formatted);
  return bytes_formatted;
}

size_t FormatLineNumber(int line, turbo::span<char>& buf) {
  constexpr size_t kLineFieldMaxLen =
      sizeof(":] ") + (1 + std::numeric_limits<int>::digits10 + 1) - sizeof("");
  if (TURBO_UNLIKELY(buf.size() < kLineFieldMaxLen)) {
    // As above, we don't bother trying to truncate this if the buffer is too
    // short and it should never happen.
    buf.remove_suffix(buf.size());
    return 0;
  }
  char* p = buf.data();
  *p++ = ':';
  p = turbo::numbers_internal::FastIntToBuffer(line, p);
  *p++ = ']';
  *p++ = ' ';
  const size_t bytes_formatted = static_cast<size_t>(p - buf.data());
  buf.remove_prefix(bytes_formatted);
  return bytes_formatted;
}

}  // namespace

std::string FormatLogMessage(turbo::LogSeverity severity,
                             turbo::CivilSecond civil_second,
                             turbo::Duration subsecond, log_internal::Tid tid,
                             std::string_view basename, int line,
                             PrefixFormat format, std::string_view message) {
  return turbo::str_format(
      "%c%02d%02d %02d:%02d:%02d.%06d %7d %s:%d] %s%s",
      turbo::LogSeverityName(severity)[0], civil_second.month(),
      civil_second.day(), civil_second.hour(), civil_second.minute(),
      civil_second.second(), turbo::Duration::to_microseconds(subsecond), tid,
      basename, line, format == PrefixFormat::kRaw ? "RAW: " : "", message);
}

// This method is fairly hot, and the library always passes a huge `buf`, so we
// save some bounds-checking cycles by not trying to do precise truncation.
// Truncating at a field boundary is probably a better UX anyway.
//
// The prefix is written in three parts, each of which does a single
// bounds-check and truncation:
// 1. severity, timestamp, and thread ID
// 2. filename
// 3. line number and bracket
size_t FormatLogPrefix(turbo::LogSeverity severity, turbo::Time timestamp,
                       log_internal::Tid tid, std::string_view basename,
                       int line, PrefixFormat format, turbo::span<char>& buf) {
  auto prefix_size = FormatBoundedFields(severity, timestamp, tid, buf);
  prefix_size += log_internal::AppendTruncated(basename, buf);
  prefix_size += FormatLineNumber(line, buf);
  if (format == PrefixFormat::kRaw)
    prefix_size += log_internal::AppendTruncated("RAW: ", buf);
  return prefix_size;
}

}  // namespace log_internal
TURBO_NAMESPACE_END
}  // namespace turbo
